﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Xml;

using Storm.TextEditor.Editor;
using Storm.TextEditor.Languages;
using Storm.TextEditor.Parser.Objects;
using Storm.TextEditor.Parser.Objects.Collections;

namespace Storm.TextEditor.Parser.XML
{
    /// <summary>
    /// Represents a class that reads language definitions from XML sheets.
    /// </summary>
    public class LanguageReader
    {
        #region Fields

        private Hashtable styles = new Hashtable();
        private Hashtable blocks = new Hashtable();

        private Language language = new Language();
        private LanguageList parsedLanguages = new LanguageList();

        private static bool useUserCustomStyles = false;
        private static string userCustomStyles = null;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the custom user styles.
        /// </summary>
        public static string UserCustomStyles
        {
            get { return LanguageReader.userCustomStyles; }
            set { LanguageReader.userCustomStyles = value; }
        }

        /// <summary>
        /// Gets or sets whether to use custom user styles.
        /// </summary>
        public static bool UseUserCustomStyles
        {
            get { return useUserCustomStyles; }
            set { useUserCustomStyles = value; }
        }

        #endregion

        #region Methods

        #region Public

        /// <summary>
        /// Loads the given file.
        /// </summary>
        /// <param name="filename">File to load.</param>
        /// <returns>Language found in the given file.</returns>
        public Language Load(string filename)
        {
            return Load(File.OpenRead(filename));
        }

        /// <summary>
        /// Loads the given file.
        /// </summary>
        /// <param name="stream">File to load.</param>
        /// <returns>Language found in the given file.</returns>
        public Language Load(Stream stream)
        {
            styles = new Hashtable();
            blocks = new Hashtable();
            language = new Language();

            XmlDocument langDocument = new XmlDocument();
            langDocument.Load(stream);

            if (useUserCustomStyles == true && Directory.Exists(userCustomStyles) == true)
            {
                string langName = langDocument.SelectSingleNode("Language/@Name").InnerText;
                string path = Path.Combine(LanguageReader.UserCustomStyles, langName + ".conf");

                if (File.Exists(path) == true)
                {
                    XmlDocument userConf = new XmlDocument();
                    userConf.Load(path);

                    XmlNodeList nodeList = langDocument.SelectNodes("Language/Style");
                    foreach (XmlNode current in nodeList)
                    {
                        XmlNode userStyleNode = userConf.SelectSingleNode("styles/Style[@Name='" +
                            current.Attributes["Name"].InnerText + "']");

                        if (userStyleNode == null)
                            continue;

                        foreach (XmlAttribute userAtt in userStyleNode.Attributes)
                            current.Attributes[userAtt.LocalName].InnerText = userAtt.InnerText;
                    }
                }
            }

            this.ReadLanguageDefinition(langDocument);
            return language;
        }

        /// <summary>
        /// Loads the given file.
        /// </summary>
        /// <param name="filename">File to load.</param>
        /// <param name="separators">Separators to use when reading the file.</param>
        /// <returns>Language found in the given file.</returns>
        public Language Load(string filename, string separators)
        {
            styles = new Hashtable();
            blocks = new Hashtable();

            language = new Language();
            language.Separators = separators;

            XmlDocument document = new XmlDocument();
            document.Load(filename);

            this.ReadLanguageDefinition(document);
            return language;
        }

        /// <summary>
        /// Loads the given XML.
        /// </summary>
        /// <param name="xml">XML to load.</param>
        /// <returns>Language found in the given XML.</returns>
        public Language LoadXml(string xml)
        {
            styles = new Hashtable();
            blocks = new Hashtable();

            language = new Language();

            XmlDocument document = new XmlDocument();
            document.LoadXml(xml);

            this.ReadLanguageDefinition(document);
            return language;
        }

        #endregion

        #region Private

        /// <summary>
        /// Reads the given XmlDocument's first Language XmlNode.
        /// </summary>
        /// <param name="document">Document to read.</param>
        private void ReadLanguageDefinition(XmlDocument document)
        {
            this.ParseLanguage(document["Language"]);
        }

        /// <summary>
        /// Parses the given XmlNode and its children for languages.
        /// </summary>
        /// <param name="node">XmlNode to parse.</param>
        private void ParseLanguage(XmlNode node)
        {
            string name = "";
            string startBlock = "";

            foreach (XmlAttribute att in node.Attributes)
            {
                if (att.Name.ToLower() == "name")
                    name = att.Value;

                if (att.Name.ToLower() == "startblock")
                    startBlock = att.Value;
            }

            language.Name = name;
            language.MainBlock = this.GetBlock(startBlock);

            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    switch (childNode.Name.ToLower())
                    {
                        case "filetypes":
                            this.ParseFileTypes(childNode);
                            break;
                        case "inherits":
                            this.ParseInherits(childNode);
                            break;
                        case "block":
                            this.ParseBlock(childNode);
                            break;
                        case "style":
                            this.ParseStyle(childNode);
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Parses the given XmlNode and its children for file types.
        /// </summary>
        /// <param name="node">XmlNode to parse.</param>
        private void ParseFileTypes(XmlNode node)
        {
            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    if (childNode.Name.ToLower() == "filetype")
                    {
                        string extension = "";
                        string name = "";

                        foreach (XmlAttribute att in childNode.Attributes)
                        {
                            switch (att.Name.ToLower())
                            {
                                case "name":
                                    name = att.Value;
                                    break;
                                case "extension":
                                    extension = att.Value;
                                    break;
                            }
                        }

                        FileType fileType = new FileType();
                        fileType.Extension = extension;
                        fileType.Name = name;

                        language.FileTypes.Add(fileType);
                    }
                }
            }
        }

        /// <summary>
        /// Parses the given XmlNode and its children for inheritance.
        /// </summary>
        /// <param name="node">XmlNode to parse.</param>
        private void ParseInherits(XmlNode node)
        {
            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    if (childNode.Name.ToLower() == "inherit")
                    {
                        string file = "";

                        foreach (XmlAttribute attribute in childNode.Attributes)
                        {
                            switch (attribute.Name.ToLower())
                            {
                                case "file":
                                    file = attribute.Value;
                                    break;
                            }
                        }

                        Hashtable tempBlocks = blocks;
                        Hashtable tempStyles = styles;

                        Language tempLanguage = language;
                        int localIndex = file.ToLower().IndexOf("local:");
                        bool local = localIndex > -1;

                        Language localLanguage = null;
                        if (local == true)
                        {
                            string fileName = file.Remove(localIndex, "local:".Length);
                            Stream fileStream = typeof(LanguageLoader).Assembly.GetManifestResourceStream("Storm.TextEditor.Languages." + fileName);

                            localLanguage = this.Load(fileStream);
                        }
                        else
                        {
                            Stream fileStream = File.Open(file, FileMode.Open);
                            localLanguage = this.Load(fileStream);
                        }

                        blocks = tempBlocks;
                        styles = tempStyles;

                        language = tempLanguage;
                        if (localLanguage != null)
                        {
                            language.DerivedLanguages.Add(localLanguage);

                            foreach (TextStyle style in localLanguage.Styles)
                            {
                                if (string.IsNullOrEmpty(style.Name) == false)
                                    styles.Add(style.Name, style);
                            }

                            foreach (Block block in localLanguage.Blocks)
                            {
                                if (blocks.ContainsKey(block.Name) == false)
                                    blocks.Add(block.Name, block);
                            }

                            foreach (PatternList patternList in localLanguage.MainBlock.KeywordsList)
                                language.MainBlock.KeywordsList.Add(patternList);

                            foreach (PatternList patternList in localLanguage.MainBlock.OperatorsList)
                                language.MainBlock.OperatorsList.Add(patternList);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Parses the given XmlNode and its children for blocks.
        /// </summary>
        /// <param name="node">XmlNode to parse.</param>
        private void ParseBlock(XmlNode node)
        {
            string name = "";
            string style = "";
            string patternStyle = "";

            bool multiline = false;
            bool terminateChildren = false;

            Color backColor = Color.Transparent;

            foreach (XmlAttribute att in node.Attributes)
            {
                switch (att.Name.ToLower())
                {
                    case "name":
                        name = att.Value;
                        break;
                    case "style":
                        style = att.Value;
                        break;
                    case "patternstyle":
                        patternStyle = att.Value;
                        break;
                    case "ismultiline":
                        multiline = bool.Parse(att.Value);
                        break;
                    case "terminatechildren":
                        terminateChildren = bool.Parse(att.Value);
                        break;
                    case "backcolor":
                        backColor = Color.FromName(att.Value);
                        break;
                }
            }

            Block blockList = GetBlock(name);
            blockList.BackColor = backColor;
            blockList.Name = name;
            blockList.Multiline = multiline;
            blockList.Style = GetStyle(style);
            blockList.TerminateChildren = terminateChildren;

            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    if (childNode.Name.ToLower() == "indent")
                    {
                        string start = "";
                        string end = "";
                        string nodeStyle = "";

                        bool startIsComplex = false;
                        bool startIsKeyword = true;
                        bool endIsComplex = false;
                        bool endIsKeyword = true;

                        foreach (XmlAttribute att in childNode.Attributes)
                        {
                            switch (att.Name.ToLower())
                            {
                                case "start":
                                    start = att.Value;
                                    break;
                                case "end":
                                    end = att.Value;
                                    break;
                                case "startiscomplex":
                                    startIsComplex = bool.Parse(att.Value);
                                    break;
                                case "startiskeyword":
                                    startIsKeyword = bool.Parse(att.Value);
                                    break;
                                case "endiscomplex":
                                    endIsComplex = bool.Parse(att.Value);
                                    break;
                                case "endiskeyword":
                                    endIsKeyword = bool.Parse(att.Value);
                                    break;
                                case "style":
                                    nodeStyle = att.Value;
                                    break;
                            }
                        }

                        if (start != "")
                        {
                            Scope scope = new Scope();

                            scope.CauseIndent = true;
                            scope.Indenter = true;
                            scope.DefaultExpanded = true;

                            scope.Style = this.GetStyle(nodeStyle);
                            scope.ExpansionText = start + "..." + end;

                            Pattern startPattern = new Pattern(start, startIsComplex, false, startIsKeyword);
                            Pattern endPattern = new Pattern(end, endIsComplex, false, endIsKeyword);

                            scope.Start = startPattern;
                            scope.EndPatterns.Add(endPattern);
                            blockList.ScopePatterns.Add(scope);
                        }
                    }

                    if (childNode.Name.ToLower() == "scope")
                    {
                        string start = "";
                        string end = "";
                        string nodeStyle = "";
                        string text = "";

                        string startIsSeparator = "";
                        string endIsSeparator = "";

                        string startIsComplex = "false";
                        string endIsComplex = "false";

                        string startIsKeyword = "false";
                        string endIsKeyword = "false";

                        string spawnStart = "";
                        string spawnEnd = "";

                        string escapeChar = "";
                        string causeIndent = "false";
                        List<string> modifiers = new List<string>();

                        bool expanded = true;

                        foreach (XmlAttribute att in childNode.Attributes)
                        {
                            switch (att.Name.ToLower())
                            {
                                case "start":
                                    start = att.Value;
                                    break;
                                case "end":
                                    end = att.Value;
                                    break;
                                case "escapechar":
                                    escapeChar = att.Value;
                                    break;
                                case "style":
                                    nodeStyle = att.Value;
                                    break;
                                case "text":
                                    text = att.Value;
                                    break;
                                case "defaultexpanded":
                                    expanded = bool.Parse(att.Value);
                                    break;
                                case "endisseparator":
                                    endIsSeparator = att.Value;
                                    break;
                                case "startisseparator":
                                    startIsSeparator = att.Value;
                                    break;
                                case "startiskeyword":
                                    startIsKeyword = att.Value;
                                    break;
                                case "startiscomplex":
                                    startIsComplex = att.Value;
                                    break;
                                case "endiscomplex":
                                    endIsComplex = att.Value;
                                    break;
                                case "endiskeyword":
                                    endIsKeyword = att.Value;
                                    break;
                                case "spawnblockonstart":
                                    spawnStart = att.Value;
                                    break;
                                case "spawnblockonend":
                                    spawnEnd = att.Value;
                                    break;
                                case "causeindent":
                                    causeIndent = att.Value;
                                    break;
                                case "modifiers":
                                    string[] splitValue = att.Value.Split(',');
                                    foreach (string modifier in splitValue)
                                        modifiers.Add(modifier.Trim());

                                    break;
                            }
                        }

                        if (start != "")
                        {
                            Scope scope = new Scope();
                            scope.Style = this.GetStyle(nodeStyle);
                            scope.ExpansionText = text;
                            scope.Modifiers = modifiers;
                            scope.DefaultExpanded = expanded;

                            bool boolStartIsComplex = bool.Parse(startIsComplex);
                            bool boolEndIsComplex = bool.Parse(endIsComplex);
                            bool boolCauseIndent = bool.Parse(causeIndent);

                            scope.CauseIndent = boolCauseIndent;

                            Pattern startPattern = new Pattern(start, boolStartIsComplex, false, bool.Parse(startIsKeyword));
                            Pattern endPattern = escapeChar != "" ? new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword), escapeChar) :
                                new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword));

                            if (endIsSeparator != "")
                                endPattern.IsSeparator = bool.Parse(endIsSeparator);

                            scope.Start = startPattern;
                            scope.EndPatterns.Add(endPattern);
                            blockList.ScopePatterns.Add(scope);

                            if (spawnStart != "")
                                scope.SpawnBlockOnStart = this.GetBlock(spawnStart);

                            if (spawnEnd != "")
                                scope.SpawnBlockOnEnd = this.GetBlock(spawnEnd);
                        }
                    }

                    if (childNode.Name.ToLower() == "bracket")
                    {
                        string start = "";
                        string end = "";
                        string childStyle = "";

                        string endIsSeparator = "";
                        string startIsSeparator = "";

                        string startIsComplex = "false";
                        string endIsComplex = "false";

                        string startIsKeyword = "false";
                        string endIsKeyword = "false";
                        string childMultiline = "true";

                        foreach (XmlAttribute att in childNode.Attributes)
                        {
                            switch (att.Name.ToLower())
                            {
                                case "start":
                                    start = att.Value;
                                    break;
                                case "end":
                                    end = att.Value;
                                    break;
                                case "style":
                                    childStyle = att.Value;
                                    break;
                                case "endisseparator":
                                    endIsSeparator = att.Value;
                                    break;
                                case "startisseparator":
                                    startIsSeparator = att.Value;
                                    break;
                                case "startiskeyword":
                                    startIsKeyword = att.Value;
                                    break;
                                case "startiscomplex":
                                    startIsComplex = att.Value;
                                    break;
                                case "endiscomplex":
                                    endIsComplex = att.Value;
                                    break;
                                case "ismultiline":
                                    childMultiline = att.Value;
                                    break;
                            }
                        }

                        if (start != "")
                        {
                            PatternList patternList = new PatternList();
                            patternList.Style = this.GetStyle(childStyle);

                            bool boolStartIsComplex = bool.Parse(startIsComplex);
                            bool boolEndIsComplex = bool.Parse(endIsComplex);
                            bool boolMultiline = bool.Parse(childMultiline);

                            Pattern startPattern = new Pattern(start, boolStartIsComplex, false, bool.Parse(startIsKeyword));
                            Pattern endPattern = new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword));

                            startPattern.MatchingBracket = endPattern;
                            endPattern.MatchingBracket = startPattern;

                            startPattern.BracketType = BracketType.StartBracket;
                            endPattern.BracketType = BracketType.EndBracket;

                            startPattern.IsMultilineBracket = endPattern.IsMultilineBracket = boolMultiline;

                            patternList.Add(startPattern);
                            patternList.Add(endPattern);
                            blockList.OperatorsList.Add(patternList);
                        }
                    }
                }

                if (childNode.Name.ToLower() == "keywords")
                {
                    foreach (XmlNode exChildNode in childNode.ChildNodes)
                    {
                        if (exChildNode.Name.ToLower() == "patterngroup")
                        {
                            PatternList patternList = new PatternList();
                            blockList.KeywordsList.Add(patternList);

                            foreach (XmlAttribute att in exChildNode.Attributes)
                            {
                                switch (att.Name.ToLower())
                                {
                                    case "name":
                                        patternList.Name = att.Value;
                                        break;
                                    case "style":
                                        patternList.Style = GetStyle(att.Value);
                                        break;
                                    case "normalizecase":
                                        patternList.NormalizeCase = bool.Parse(att.Value);
                                        break;
                                    case "casesensitive":
                                        patternList.CaseSensitive = bool.Parse(att.Value);
                                        break;
                                }
                            }

                            foreach (XmlNode moreExChildNode in exChildNode.ChildNodes)
                            {
                                if (moreExChildNode.Name.ToLower() == "pattern")
                                {
                                    bool complex = false;
                                    bool separator = false;

                                    string category = "";
                                    string pattern = "";

                                    if (moreExChildNode.Attributes != null)
                                    {
                                        foreach (XmlAttribute att in moreExChildNode.Attributes)
                                        {
                                            switch (att.Name.ToLower())
                                            {
                                                case "text":
                                                    pattern = att.Value;
                                                    break;
                                                case "iscomplex":
                                                    complex = bool.Parse(att.Value);
                                                    break;
                                                case "isseparator":
                                                    separator = bool.Parse(att.Value);
                                                    break;
                                                case "category":
                                                    category = (att.Value);
                                                    break;
                                            }
                                        }
                                    }

                                    if (pattern != "")
                                    {
                                        Pattern exPattern = new Pattern(pattern, complex, separator, true);
                                        exPattern.Category = category;
                                        patternList.Add(exPattern);
                                    }
                                }
                                else if (moreExChildNode.Name.ToLower() == "patterns")
                                {
                                    string patterns = moreExChildNode.ChildNodes[0].Value;
                                    patterns = patterns.Replace("\t", " ");

                                    while (patterns.IndexOf("  ") >= 0)
                                        patterns = patterns.Replace("  ", " ");

                                    foreach (string pattern in patterns.Split())
                                    {
                                        if (pattern != "")
                                            patternList.Add(new Pattern(pattern, false, false, true));
                                    }
                                }
                            }
                        }
                    }
                }

                if (childNode.Name.ToLower() == "operators")
                {
                    foreach (XmlNode exChildNode in childNode.ChildNodes)
                    {
                        if (exChildNode.Name.ToLower() == "patterngroup")
                        {
                            PatternList patternList = new PatternList();
                            blockList.OperatorsList.Add(patternList);
                            foreach (XmlAttribute att in exChildNode.Attributes)
                            {
                                switch (att.Name.ToLower())
                                {
                                    case "style":
                                        patternList.Style = GetStyle(att.Value);
                                        break;
                                    case "name":
                                        patternList.Name = att.Value;
                                        break;
                                    case "normalizecase":
                                        patternList.NormalizeCase = bool.Parse(att.Value);
                                        break;
                                    case "casesensitive":
                                        patternList.CaseSensitive = bool.Parse(att.Value);
                                        break;
                                }
                            }

                            foreach (XmlNode moreExChildNode in exChildNode.ChildNodes)
                            {
                                if (moreExChildNode.Name.ToLower() == "pattern")
                                {
                                    bool complex = false;
                                    bool separator = false;

                                    string pattern = "";
                                    string category = null;

                                    if (moreExChildNode.Attributes != null)
                                    {
                                        foreach (XmlAttribute att in moreExChildNode.Attributes)
                                        {
                                            switch (att.Name.ToLower())
                                            {
                                                case "text":
                                                    pattern = att.Value;
                                                    break;
                                                case "iscomplex":
                                                    complex = bool.Parse(att.Value);
                                                    break;
                                                case "isseparator":
                                                    separator = bool.Parse(att.Value);
                                                    break;
                                                case "category":
                                                    category = (att.Value);
                                                    break;
                                            }
                                        }
                                    }

                                    if (pattern != "")
                                    {
                                        Pattern exPattern = new Pattern(pattern, complex, separator, false);
                                        exPattern.Category = category;
                                        patternList.Add(exPattern);
                                    }
                                }
                                else if (moreExChildNode.Name.ToLower() == "patterns")
                                {
                                    string patterns = moreExChildNode.ChildNodes[0].Value;
                                    patterns = patterns.Replace("\t", " ");

                                    while (patterns.IndexOf("  ") >= 0)
                                        patterns = patterns.Replace("  ", " ");

                                    string[] splittedPatterns = patterns.Split();
                                    foreach (string pattern in splittedPatterns)
                                    {
                                        if (pattern != "")
                                            patternList.Add(new Pattern(pattern, false, false, false));
                                    }
                                }
                            }
                        }
                    }
                }

                if (childNode.Name.ToLower() == "childblocks")
                {
                    foreach (XmlNode exChildNode in childNode.ChildNodes)
                    {
                        if (exChildNode.Name.ToLower() == "child")
                        {
                            foreach (XmlAttribute att in exChildNode.Attributes)
                            {
                                if (att.Name.ToLower() == "name")
                                    blockList.ChildBlocks.Add(GetBlock(att.Value));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Parses the given XmlNode and its children for styles.
        /// </summary>
        /// <param name="node">XmlNode to parse.</param>
        private void ParseStyle(XmlNode node)
        {
            string name = "";

            string foreColor = "";
            string backColor = "";

            bool bold = false;
            bool italic = false;
            bool underline = false;

            foreach (XmlAttribute att in node.Attributes)
            {
                switch (att.Name.ToLower())
                {
                    case "name":
                        name = att.Value;
                        break;
                    case "forecolor":
                        foreColor = att.Value;
                        break;
                    case "backcolor":
                        backColor = att.Value;
                        break;
                    case "bold":
                        bold = bool.Parse(att.Value);
                        break;
                    case "italic":
                        italic = bool.Parse(att.Value);
                        break;
                    case "underline":
                        underline = bool.Parse(att.Value);
                        break;
                }
            }

            TextStyle textStyle = GetStyle(name);

            if (backColor != "")
                textStyle.BackColor = Color.FromName(backColor);

            if (foreColor.IndexOf(",") != -1)	// If the ForeColor string has "," in it,
            {									// it is an RGB color.
                foreColor = foreColor.Trim();
                string[] rgb = foreColor.Split(',');

                string sR = rgb[0];
                string sG = rgb[1];
                string sB = rgb[2];

                int r = Convert.ToInt32(sR);
                int g = Convert.ToInt32(sG);
                int b = Convert.ToInt32(sB);

                textStyle.ForeColor = Color.FromArgb(r, g, b);
            }
            else
                textStyle.ForeColor = Color.FromName(foreColor);

            textStyle.Bold = bold;
            textStyle.Italic = italic;
            textStyle.Underline = underline;
            textStyle.Name = name;
        }

        /// <summary>
        /// Returns the TextStyle representing the given name.
        /// </summary>
        /// <param name="name">Name to find TextStyle for.</param>
        /// <returns>The TextStyle representing the given name.</returns>
        private TextStyle GetStyle(string name)
        {
            if (styles[name] == null)
            {
                TextStyle textStyle = new TextStyle();
                styles.Add(name, textStyle);
            }

            return styles[name] as TextStyle;
        }

        /// <summary>
        /// Returns the block representing the given name.
        /// </summary>
        /// <param name="name">Name to find block for.</param>
        /// <returns>The block representing the given name.</returns>
        private Block GetBlock(string name)
        {
            if (blocks[name] == null)
            {
                Block block = new Block(language);
                blocks.Add(name, block);
            }

            return blocks[name] as Block;
        }

        #endregion

        #endregion

        /// <summary>
        /// Initializes a new instance of LanguageReader.
        /// </summary>
        public LanguageReader()
        {
        }
    }
}